/**
  glx-window.c - Created by Timothy Morey on 3/12/2012.

  This file provides an implementation of the window.h interface using the GLX
  extensions for the X Window System.

  Large portions of this file were borrowed from
    http://www.codesampler.com/linuxsrc.htm.
  The following was in the header of the primary source file:
  *           Name: ogl_glx_sample.cpp
  *         Author: Kevin Harris
  *  Last Modified: 08/06/04
  *    Description: This sample is a basic demonstration of how to use GLX to
  *                 create windowed OpenGL samples via XFree86, which is a a
  *                 freely redistributable open-source implementation of the
  *                 X Window System.
*/

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <X11/Xatom.h>

#include "communication-queue.h"
#include "err.h"
#include "layer.h"
#include "logger.h"
#include "thread-utils.h"
#include "window.h"

#define MAX_WINDOWS 32

/* compile error if we add these before the local includes... */
#include <GL/gl.h>
#include <GL/glx.h>


/******************************************************************************
 * Local declarations:
 ******************************************************************************/

typedef struct
{
  int WindowID;  /* The window handle that we give the user to identify this
                    window */
  Window Window;
  GLXContext GLXContext;
  int Layer;
  int X;
  int Y;
  int Width;
  int Height;
  int Borderless;
} WindowDef;

typedef struct
{
  WindowDef* Windows[MAX_WINDOWS];
  Display* Display;
  XVisualInfo* VisualInfo;
  Thread UIThread;
  CommunicationQueue Queue;
  int KeepUIThreadAlive;
} GraphicsContext;

/**
   The Hints structure is based on the Motif hints structure and is used to maka a
   borderless window.  Based on code found here:
     http://tonyobryan.com/index.php?article=9
 */
typedef struct
{
  unsigned long Flags;
  unsigned long Functions;
  unsigned long Decorations;
  long InputMode;
  unsigned long Status;
} Hints;

GraphicsContext g_Context;

int InitUIThread();
void RunMessageLoop();
void ShutdownUIThread();
void* UIThreadEntryPoint(void* arg);


/******************************************************************************
 * Implementation of window.h functions:
 ******************************************************************************/

int AddLayer(int windowID, int layer)
{
  int retval = NVN_NOERR;
  int iscur = 0;

  if(NVN_NOERR == IsCurrentThread(g_Context.UIThread, &iscur) && !iscur)
  {
    Message msg;
    int handled = 0;

    InitMessage(&msg, "AddLayer");
    msg.Arguments[0] = &windowID;
    msg.Arguments[1] = &layer;
    msg.Handled = &handled;
    msg.Result = &retval;
    msg.ResultSize = sizeof(int);
    Push(&g_Context.Queue, msg);

    /* Wait for the message to be handled, since we want a return value. */
    while(! handled)
      pthread_yield();
  }
  else
  {
    WindowDef* win = 0;

    if(windowID >= 0 && windowID < MAX_WINDOWS)
      win = g_Context.Windows[windowID];
    else
      retval = NVN_EINVARGS;

    if(win)
    {
			win->Layer = layer;
      UpdateLayerTransform(win->Layer, win->Width, win->Height);
    }
  }

  return retval;
}

int CreateGLWindow(char* title, int x, int y, int width, int height, int borderless,
                   int* window)
{
  int retval = NVN_NOERR;
  int iscur = 0;

  if(NVN_NOERR == IsCurrentThread(g_Context.UIThread, &iscur) && !iscur)
  {
    Message msg;
    int handled = 0;

    /* printf("CreateGLWindow - wrong thread\n"); */

    InitMessage(&msg, "CreateGLWindow");
    msg.Arguments[0] = &title;
    msg.Arguments[1] = &x;
    msg.Arguments[2] = &y;
    msg.Arguments[3] = &width;
    msg.Arguments[4] = &height;
    msg.Arguments[5] = &borderless;
    msg.Arguments[6] = &window;
    msg.Handled = &handled;
    msg.Result = &retval;
    msg.ResultSize = sizeof(int);
    Push(&g_Context.Queue, msg);

    /* Wait for the message to be handled, since we want a return value. */
    while(! handled)
      ThreadYield();
  }
  else
  {
    int i = 0;        /* Loop counter */
    WindowDef* win = 0;  /* The new window */
    XSetWindowAttributes windowAttrs;
    Colormap colormap;

    /* printf("CreateGLWindow - right thread\n"); */
  
    win = (WindowDef*)malloc(sizeof(WindowDef));
    memset(win, 0, sizeof(WindowDef));
    win->WindowID = -1;
    win->X = x;
    win->Y = y;
    win->Width = width;
    win->Height = height;
    win->Borderless = borderless;
		win->Layer = INVALID_LAYER;
    for(i = 0; i < MAX_WINDOWS; i++)
    {
      if(g_Context.Windows[i] == 0)
      {
        win->WindowID = i;
        g_Context.Windows[i] = win;
        break;
      }
    }
    
    if(win->WindowID < 0)
    {
      free(win);
      win = 0;
      retval = NVN_EMAXOBJS;
      WriteMessage(LogLevelError, "MAX_WINDOWS exceeded");
    }
    
    if(win)
    {
      win->GLXContext = glXCreateContext(g_Context.Display,
                                         g_Context.VisualInfo,
                                         0,
                                         1);
      
      if(! win->GLXContext)
      {
        DestroyGLWindow(win->WindowID);
        win = 0;
        retval = NVN_EGLXFAIL;
        WriteMessage(LogLevelError, "Unable to create a GLX context.");
      }
    }
    
    if(win)
    {
      colormap = 
        XCreateColormap(g_Context.Display,
                        RootWindow(g_Context.Display, g_Context.VisualInfo->screen),
                        g_Context.VisualInfo->visual,
                        AllocNone);
    }
    
    if(win)
    {
      windowAttrs.colormap = colormap;
      windowAttrs.border_pixel = 0;
      windowAttrs.event_mask = 
        ExposureMask           |
        VisibilityChangeMask   |
        KeyPressMask           |
        KeyReleaseMask         |
        ButtonPressMask        |
        ButtonReleaseMask      |
        PointerMotionMask      |
        StructureNotifyMask    |
        SubstructureNotifyMask |
        FocusChangeMask;
      windowAttrs.override_redirect = 1;
    
      win->Window = 
        XCreateWindow(g_Context.Display,
                      RootWindow(g_Context.Display, g_Context.VisualInfo->screen),
                      x,
                      y,
                      width,
                      height,
                      0,
                      g_Context.VisualInfo->depth,
                      InputOutput,
                      g_Context.VisualInfo->visual,
                      CWBorderPixel | CWColormap | CWEventMask | CWOverrideRedirect,
                      &windowAttrs);
      
      XSetStandardProperties(g_Context.Display,
                             win->Window,
                             title,
                             title,
                             None,
                             0,
                             0,
                             0);

      if(borderless)
      {
        Hints hints;
        Atom prop;
        hints.Flags = 2;
        hints.Decorations = 0;
        prop = XInternAtom(g_Context.Display, "_MOTIF_WM_HINTS", True);
        XChangeProperty(g_Context.Display,
                        win->Window,
                        prop,
                        prop,
                        32,
                        PropModeReplace,
                        (unsigned char *)&hints,
                        5);
      }
      
      glXMakeCurrent(g_Context.Display, win->Window, win->GLXContext);
      XMapWindow(g_Context.Display, win->Window);

      glClearColor(0.0, 0.0, 0.0, 1.0);
      glClear(GL_COLOR_BUFFER_BIT);

      glXSwapBuffers(g_Context.Display, win->Window);
      *window = win->WindowID;
    }
  }
  
  return retval;
}

int DestroyGLWindow(int windowID)
{
  int retval = NVN_NOERR;
  int iscur = 0;

  if(NVN_NOERR == IsCurrentThread(g_Context.UIThread, &iscur) && !iscur)
  {
    Message msg;

    /* printf("DestroyGLWindow - wrong thread\n"); */

    InitMessage(&msg, "DestroyGLWindow");
    msg.Arguments[0] = malloc(sizeof(int));
    msg.DestroyArgs = 1;
    *((int*)msg.Arguments[0]) = windowID;
    Push(&g_Context.Queue, msg);
  }
  else
  {
    WindowDef* win = 0;

    /* printf("DestroyGLWindow - right thread\n"); */

    if(windowID >= 0 && windowID < MAX_WINDOWS && 
       g_Context.Windows[windowID])
    {
      win = g_Context.Windows[windowID];
      if(win->Window)
      {
        XDestroyWindow(g_Context.Display, win->Window);
        win->Window = 0;
      }
    }
    else
    {
      retval = NVN_EINVARGS;
    }
    
    if(win)
    {
      free(win);
      g_Context.Windows[windowID] = 0;
    }
  }

  return retval;
}

int RedrawGLWindow(int window)
{
  int retval = NVN_NOERR;
  int iscur = 0;

  if(NVN_NOERR == IsCurrentThread(g_Context.UIThread, &iscur) && iscur)
  {
    Message msg;

    InitMessage(&msg, "RedrawGLWindow");
    msg.Arguments[0] = malloc(sizeof(int));
    msg.DestroyArgs = 1;
    *((int*)msg.Arguments[0]) = window;
    Push(&g_Context.Queue, msg);
  }
  else
  {
    WindowDef* win = 0;
		XEvent event;

    if(window >= 0 && window < MAX_WINDOWS)
      win = g_Context.Windows[window];
    else
      retval = NVN_EINVARGS;

    if(win)
    {
			memset(&event, 0, sizeof(XEvent));
			event.type = Expose;
			event.xexpose.window = win->Window;
			XSendEvent(g_Context.Display, win->Window, 0, ExposureMask, &event);
    }
  }

  return retval;
}

int StartUIThread()
{
  int retval = NVN_NOERR;

  memset(&g_Context, 0, sizeof(GraphicsContext));
  InitQueue(&g_Context.Queue);
  g_Context.KeepUIThreadAlive = 1;
  if(0 != pthread_create(&g_Context.UIThread.Handle, 0, UIThreadEntryPoint, 0))
  {
    retval = NVN_ETHREADFAIL;
  }

  return retval;
}

int StopUIThread()
{
  int retval = NVN_NOERR;

  g_Context.KeepUIThreadAlive = 0;
  if(0 != pthread_join(g_Context.UIThread.Handle, 0))
  {
    retval = NVN_ETHREADFAIL;
  }

  return retval;
}


/******************************************************************************
 * Implementation of local functions:
 ******************************************************************************/

int InitUIThread()
{
  int retval = NVN_NOERR;
  int doubleBufferVisual[] =  /* A description of the GLX visual we want. */
  {
    GLX_RGBA,
    GLX_DEPTH_SIZE, 16,
    GLX_DOUBLEBUFFER,
    None
  };
  
  memset(g_Context.Windows, 0, MAX_WINDOWS * sizeof(WindowDef*));

  g_Context.Display = XOpenDisplay(0);
  if(! g_Context.Display)
  {
    retval = NVN_EXWINFAIL;
    WriteMessage(LogLevelError, "XOpenDisplay failed");
  }
  else if(! glXQueryExtension(g_Context.Display, 0, 0))
  {
    retval = NVN_EGLXFAIL;
    WriteMessage(LogLevelError, "X server doesn't have the OpenGL GLX extension.");
  }

  if(NVN_NOERR == retval)
  {
    g_Context.VisualInfo = 
      glXChooseVisual(g_Context.Display, 
		      DefaultScreen(g_Context.Display), 
		      doubleBufferVisual);

    if(! g_Context.VisualInfo)
    {
      retval = NVN_EGLXFAIL;
      WriteMessage(LogLevelError,"Unable to find an appropriate GLX visual.");
    }
  }

  return retval;
}

void RunMessageLoop()
{
  XEvent event;
  Message msg;
	int valid = 0;
  int i = 0;
  int counter = 0;
  double elapsed = 0.0;
  struct timespec start, stop;

  clock_gettime(CLOCK_MONOTONIC, &start);

  while(g_Context.KeepUIThreadAlive)
  {
	  if(g_Context.Queue.Size > 0)
    {
      /* Local messages are given highest priority */

			Pop(&g_Context.Queue, &msg, &valid);
			if(valid)
			{
        int ret = NVN_NOERR;

				/* printf("Handling internal message: %s\n", msg.Message); */
				if(0 == strcmp("CreateGLWindow", msg.Message))
				{
					ret = CreateGLWindow(*((char**)msg.Arguments[0]), 
                               *((int*)msg.Arguments[1]),
                               *((int*)msg.Arguments[2]), 
                               *((int*)msg.Arguments[3]), 
                               *((int*)msg.Arguments[4]),
                               *((int*)msg.Arguments[5]),
                               *((int**)msg.Arguments[6]));
					
					if(msg.Handled)
						*msg.Handled = 1;
				}
				else if(0 == strcmp("AddLayer", msg.Message))
				{
					ret = AddLayer(*((int*)msg.Arguments[0]), 
                         *((int*)msg.Arguments[1]));
					
					if(msg.Handled)
						*msg.Handled = 1;
				}
				else if(0 == strcmp("DestroyGLWindow", msg.Message))
				{
					ret = DestroyGLWindow(*((int*)msg.Arguments[0]));
					
					if(msg.Handled)
						*msg.Handled = 1;
				}
        else if(0 == strcmp("RedrawGLWindow", msg.Message))
        {
          ret = RedrawGLWindow(*((int*)msg.Arguments[0]));
          
          if(msg.Handled)
            *msg.Handled = 1;
        }

        if(msg.Result)
          *((int*)msg.Result) = ret;
				
				if(msg.DestroyArgs)
					DestroyArguments(msg);
			}
    }
    else if(XPending(g_Context.Display))
    {
      /* printf("Handling X message\n"); */

      XNextEvent(g_Context.Display, &event);
      
      switch(event.type)
      {
        case ConfigureNotify:
        {
          WindowDef* win = 0;
          int i = 0;
          
          for(i = 0; i < MAX_WINDOWS; i++)
          {
            if(g_Context.Windows[i] && g_Context.Windows[i]->Window == event.xconfigure.window)
            {
              win = g_Context.Windows[i];
              break;
            }
          }

          if(win)
          {
            win->X = event.xconfigure.x;
            win->Y = event.xconfigure.y;
            win->Width = event.xconfigure.width;
            win->Height = event.xconfigure.height;

            if(0 <= win->Layer)
            {
              UpdateLayerTransform(win->Layer, win->Width, win->Height);
              glXSwapBuffers(g_Context.Display, win->Window);
            }
          }

          break;
        }

			  case Expose:
			  {
					WindowDef* win = 0;
          int i = 0;
					int frame = -1;
          
          for(i = 0; i < MAX_WINDOWS; i++)
          {
            if(g_Context.Windows[i] && g_Context.Windows[i]->Window == event.xexpose.window)
            {
              win = g_Context.Windows[i];
              break;
            }
          }

					if(win && 
						 NVN_NOERR == GetLayerCurrentFrame(win->Layer, &frame))
					{
						glClear(GL_COLOR_BUFFER_BIT);
						DrawLayer(win->Layer, frame);
						glXSwapBuffers(g_Context.Display, win->Window);
					}

					break;
        }
			}
    }

    ThreadSleep(1);
  }
}

void ShutdownUIThread()
{
  int i = 0;
  for(i = 0; i < MAX_WINDOWS; i++)
  {
    if(g_Context.Windows[i])
      DestroyGLWindow(i);
  }

  if(g_Context.Display)
  {
    XCloseDisplay(g_Context.Display);
    g_Context.Display = 0;
  }
}

void* UIThreadEntryPoint(void* arg)
{
  if(NVN_NOERR == InitUIThread())
  {
    RunMessageLoop();
    ShutdownUIThread();
  }
}
